import {withLock} from "easylock";
import * as request from 'superagent'

import * as config from 'config'
import * as uidString from 'uid2'
import * as  xml2js from 'xml2js'
import * as moment from 'moment'
import {InternalError} from "../api/error";
import ExtRecord from '../database/models/extRecord'
import {PlayerModel as Player} from '../database/models/player'
import {PaymentOrder, PaymentOrderModel} from "../database/models/paymentOrder";
import {IAuther} from "./autherInterface";

const XMLbuilder = new xml2js.Builder({
  rootName: 'xml',
  headless: true
})

class PrepayOrderRequired {
  appid: string
  mchId: string
  body: string
  outTradeNo: string
  totalFee: number
  spbillCreateIp: string
  timeStart: string
  timeExpire: string
  notifyUrl: string
  tradeType: string
  nonce_str: string
  productId?: string
  detail?: string
  orderId?: string
}

class PrepayOrderJSApi {
  appId: string
  timeStamp: string
  nonceStr: string
  'package': string
  signType: string
  paySign?: string
}

class PrepayOrderApp {
  appid: string
  partnerid: string
  prepayid: string
  'package': 'Sign=WXPay'
  noncestr: string
  timestamp: string
  sign?: string
}

type CreatePreOderXmlResponse = {
  xml: {
    result_code: string
    return_msg: string
    return_code: string
    prepay_id: string
  }
}

const TIMESTRING = 'YYYYMMDDHHmmss'
const PREPAYREQUESTURL = 'https://api.mch.weixin.qq.com/pay/unifiedorder'


const xml2JSON = function <T>(xmlString): Promise<T> {
  return new Promise(function (resolve, reject) {
    xml2js.parseString(xmlString, {
      explicitArray: false
    }, function (err, result) {
      if (err) return reject(err)
      resolve(result)
    })
  })
}

const camel2Underscore = function (str) {
  return str.replace(/([a-z\d])([A-Z])/g, function (matched, $1, $2) {
    return $1 + '_' + $2.toLowerCase()
  })
}

const camelKeys = (prepayOrder) => {
  const raw = {}
  Object.keys(prepayOrder).forEach(k => {
    const newKey = camel2Underscore(k)
    raw[newKey] = prepayOrder[k]
  })
  return raw
}


export default ({appid, mchId, notifyUrl}, auther) => {

  function prepayForJsApi(prepayOrder): PrepayOrderJSApi {
    const order = <PrepayOrderJSApi>{
      appId: prepayOrder.appid,
      timeStamp: Math.floor(Date.now() / 1000) + '',
      nonceStr: uidString(32),
      'package': `prepay_id=${prepayOrder.prepayId}`,
      signType: 'MD5'
    }

    const paySign = auther.sign(order)
    return {...order, paySign}
  }

  function prepayForAPP(prepayOrder): PrepayOrderApp {
    const order = <PrepayOrderApp>{
      appid: prepayOrder.appid,
      partnerid: mchId,
      prepayid: prepayOrder.prepayId,
      timestamp: Math.floor(Date.now() / 1000) + '',
      noncestr: uidString(32),
      'package': 'Sign=WXPay',
    }

    const paySign = auther.sign(order)
    return {...order, sign: paySign}
  }


  async function updatePlayerAndNoticeHim(uid, gem) {
    await Player.update({_id: uid}, {$inc: {gem: gem}})
  }

  async function finishExtRecord(extRecord) {
    extRecord.status = 'finish'
    await extRecord.save()
  }

  async function notifyPaySuccess(extRecord, extra = 0) {
    const {gem, uid} = extRecord

    await updatePlayerAndNoticeHim(uid, parseInt(gem) + extra)
    await finishExtRecord(extRecord)
  }

  async function createPrepayOrder(
    {
      orderId,
      body,
      detail,
      spbillCreateIp,
      price,
      tradeType,
      productId
    },
    afterPrepayOrderCreate: (any) => Promise<any>
  ): Promise<PrepayOrderJSApi | PrepayOrderApp> {

    const now = moment()
    const timeStart = now.format(TIMESTRING)
    const timeExpire = now.add(15, 'm').format(TIMESTRING)
    const totalFee = Math.round(price * 100)
    const outTradeNo = orderId
    const nonce_str = uidString(32)

    const prepayOrder: PrepayOrderRequired = {
      appid,
      mchId,
      body,
      detail,
      outTradeNo,
      notifyUrl,
      nonce_str,
      spbillCreateIp,
      timeStart,
      timeExpire,
      totalFee,
      tradeType,
      productId,
    }

    const orderRaw = camelKeys(prepayOrder)
    const sign = auther.sign(orderRaw)
    const xmlpr = XMLbuilder.buildObject({...orderRaw, sign})
    const res = await request.post(PREPAYREQUESTURL)
      .send(xmlpr)
      .set('Content-Type', 'application/xml')

    const resBody = res.text
    const obj: CreatePreOderXmlResponse = await xml2JSON<CreatePreOderXmlResponse>(resBody)
    const response = obj.xml
    if (response.result_code === 'SUCCESS' && response.return_code === 'SUCCESS') {
      if (auther.verify(response)) {
        const prepayId = response.prepay_id

        await afterPrepayOrderCreate(orderRaw)
        if (tradeType === 'JSAPI') {
          return prepayForJsApi({...prepayOrder, prepayId})
        } else if (tradeType === 'APP') {
          return prepayForAPP({...prepayOrder, prepayId})
        }
      }
    }

    console.log('weixinpay1', new Date(), orderRaw, response)
    throw Error('BAD_RESPONSE')
  }

  async function onNotify({xml: notification}): Promise<string> {
    const success = `<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>`
    const fail = ``

    const {out_trade_no, total_fee} = notification
    const validOrder = async () => {
      const extRecord = await ExtRecord.findOne({_id: out_trade_no}).exec()
      if (extRecord) {
        const {total_fee: totalFeeInRecord} = extRecord.extras
        const validOrder = total_fee.toString() === totalFeeInRecord.toString()
        if (validOrder && extRecord.status === 'pending') {

          await notifyPaySuccess(extRecord)
          return true
        }
      }

      return false
    }
    try {
      const valid = await validOrder()
        .catch(e => {
          console.log(`${__filename}:180 `, e)
          return false
        })

      return valid ? success : fail
    } catch (e) {
      console.log(`${__filename}:247 onNotify`, e)
      return fail
    }
  }

  return {createPrepayOrder, onNotify}
}



type PaymentArguments = {
  type: string
  order: string
  price: number
  title: string
  detail: string
  fromIP: string
}

type WechatNotification = {
  out_trade_no: string
  result_code: string
  return_code: string
  sign: string
}


export class WechatCashier {

  constructor(readonly appid: string,
              readonly mchid: string,
              readonly notifyUrl: string,
              readonly auther: IAuther) {
  }

  async createPaymentOrder(paymentArg: PaymentArguments, expiredInMs: number, now: number = Date.now()) {

    const nonce_str = uidString(32)


    const mom = moment(now)
    const timeStart = mom.format(TIMESTRING)
    const timeExpire = mom.add(expiredInMs, 'ms').format(TIMESTRING)
    const totalFee = Math.round(paymentArg.price * 100)

    const paymentOrder = new PaymentOrderModel({
      paymentType: 'wechatPay',
      orderType: paymentArg.type,
      order: paymentArg.order,
      state: 'init',
      appId: this.appid,
      mchId: this.mchid,
      price: paymentArg.price,
      createAt: new Date()
    })

    const prepayOrder: PrepayOrderRequired = {
      appid: this.appid,
      mchId: this.mchid,
      body: paymentArg.title,
      detail: paymentArg.detail,
      outTradeNo: paymentOrder.id,
      notifyUrl: this.notifyUrl,
      nonce_str,
      spbillCreateIp: paymentArg.fromIP,
      timeStart,
      timeExpire,
      totalFee,
      tradeType: 'APP',
    }

    const orderRaw = camelKeys(prepayOrder)
    const sign = this.auther.sign(orderRaw)
    const xmlpr = XMLbuilder.buildObject({...orderRaw, sign})

    const res = await request.post(PREPAYREQUESTURL)
      .send(xmlpr)
      .set('Content-Type', 'application/xml')

    const resBody = res.text
    const obj: CreatePreOderXmlResponse = await xml2JSON<CreatePreOderXmlResponse>(resBody)
    const response = obj.xml


    if (response.result_code === 'SUCCESS' && response.return_code === 'SUCCESS') {
      if (this.auther.verify(response)) {
        const prepayId = response.prepay_id

        paymentOrder.rawOrder = orderRaw
        paymentOrder.externalId = prepayId
        await paymentOrder.save()

        return {
          paymentOrder,
          prepay: this.prepayForAPP({...prepayOrder, prepayId})
        }
      }
    } else {
      throw  Error(response.return_msg)
    }
  }

  confirmPayment(confirmNotification: WechatNotification, dispatcher) {

    if (this.auther.verify(confirmNotification)) {

      const {out_trade_no} = confirmNotification

      if (confirmNotification.result_code === 'SUCCESS' && confirmNotification.return_code === 'SUCCESS') {

        return withLock(out_trade_no, async () => {

          const payment = await PaymentOrderModel.findById(out_trade_no)

          if (!payment) {
            throw InternalError('NO_SUCH_WECHAT_PAYMENT')
          }

          if (payment.state === 'init') {

            await dispatcher.confirm(payment)

            payment.state = 'finished'
            await payment.save()

          } else {
            throw InternalError('ALREADY_CONFIRMED_PAYMENT')
          }
        })
      }
    } else {
      throw InternalError('BAD_WEHCAT_PAY_NOTIFICATION')
    }
  }


  private prepayForAPP(prepayOrder): PrepayOrderApp {
    const order = <PrepayOrderApp>{
      appid: prepayOrder.appid,
      partnerid: this.mchid,
      prepayid: prepayOrder.prepayId,
      timestamp: Math.floor(Date.now() / 1000) + '',
      noncestr: uidString(32),
      'package': 'Sign=WXPay',
    }

    const paySign = this.auther.sign(order)
    return {...order, sign: paySign}
  }

}
